读《webpack 中的 watch & cache》(上)
一、前言
在现代的单页应用开发中,webpack 已经成为不可缺失的一部分,而 webpack-dev-server 给我们开发与调试带来了很大的便利。其中最有特点的部分就是 proxy 与 HMR(Hot Module Replacement)。
- HMR :对代码修改并保存后,webpack将会对代码进行打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面。
- proxy:更多的时候被我们用来代理我们的服务,主要是解决跨域的问题。
二、webpack 之 –watch
在开始了解 webpack-dev-server 之前,我们可以先了解一下 watch 是什么。
1.1 什么是 watch 服务
跟普通的 webpack 不带参数的执行方式的区别:
1 | if (firstOptions.watch || options.watch) { |
在创建 compiler 实例之后,判断是否存在 watch 参数,如果存在则以 watch 方式执行。而不是走 compuler.run 进入编译阶段。
1.2 watch 服务做了什么
1.2.1 在 watch 服务中会生成 watching 实例来接管具体的编译流程
1 | class Watching { |
在 Watching 实例上记录了编译时启动时间,运行状态,compiler 实例以及回调等。
1.2.2 执行编译
无论是 watch 模式还是编译模式,他们都是调用 compiler 实例上的 compile 方法,传入的回调函数不同
1 | compile(callback) { |
编译模式的回调函数
1 | const onCompiled = (err, compilation) => { |
watch 模式的回调函数
1 | const onCompiled = (err, compilation) => { |
watch 模式也是走之前的那套流程,解析并得到loader、文件路径绝对路径。加载 loader,loader 对文件处理。解析文件内容寻找依赖文件,重复加载与解析的过程。最后生成输出文件资源。
1.2.3 调用文件监听
在上述流程走完之后就进入主题,从 _done 方法开始。
1 | _done(err, compilation) { |
调用 watch 方法监听文件变化
1 | watch(files, dirs, missing) { |
可以看到 watch 方法通过 compiler.watchFileSystem 的 watch 方法实现,可以大致看出在文件(夹)变化触发编译后,会执行传递的回调函数,最终会调用 _invalidate 方法进行编译触发。
1 | _invalidate() { |
从上可知,watch 服务的流程主要就是重复的调用 _go -> _done -> _invalidate 这几个方法。也就是 编译
-> watch监听编译
-> 文件变更触发编译
-> 编译
的循环。
三、chokidar
在开始讲 watchFileSystem 之前,先了解一下 chokidar
1.1 chokidar 是什么
chokidar 是对 fs.watch 和 fs.watchFile 的封装,它还是依赖于 NodeJS 核心的 fs 模块,但是它比 fs.watch 和 fs.watchFile 更高效,并且拥有更好的性能。
1.1.1 chokidar 的方法和事件
chokidar.watch() 调用后会产生一个 FSWatcher 的实例,FSWatcher 拥有的方法:
- add(path / paths):文件(夹)或全局模式监听,接收一个字符串数组或者字符串。
- on(event, callback):监听 FS 事件,事件包括:
add
,addDir
,change
,unlink
,unlinkDir
,ready
,raw
,error
。 - unwatch(path / paths):停止文件(夹)或全局模式监听,接收一个字符串数组或者字符串。
- close: 移除所有监听,返回 Promise
- getWatched:返回一个对象,该对象表示此 FSWatcher 实例正在监视的文件系统上的所有路径。 对象的键是所有目录(除非使用cwd选项,否则使用绝对路径),并且值是每个目录中包含的项目名称的数组。
1.1.2 chokidar 的简单使用姿势
当文件变动的时候触发回调函数
1 | const chokidar = require('chokidar'); |
四、 watchFileSystem
1.1 初始化 watchFileSystem
其实是在创建 compiler 实例的时候,之前说过初始化 compiler 之中有 new Compiler 创建实例,有遍历插件数组,调用插件的 apply 方法,而 watchFileSystem 是通过 new NodeEnvironmentPlugin 初始化的。
1 | compiler = new Compiler(options.context);// 创建 compiler 实例 |
初始化的方式,跟包含 apply 方法的对象是一样的。这边先初始化得到 NodeEnvironmentPlugin 对象实例,再调用 apply 方法往 compuler 上挂载属性
1 | class NodeEnvironmentPlugin { |
这边有很多的 fileSystem,这边先不关注它们的区别。
1.2 监听文件变化
通过 watchFileSystem 的 watch 方法。在这个方法里,每次会拿出来旧的 watchpack 实例,并且生成新的 watchpack 实例。
1 | watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) { |
这个就是上面调用的 this.watcher.watch,这边做的事情就使用 chokidar 为文件(夹)添加监听变化事件,并且把旧的文件(夹)监听事情取消。
1 | Watchpack.prototype.watch = function watch(files, directories, startTime) { |
经过上面这些方法添加的监听事件之后,只要触发文件变动就会走进重新编译的过程。